package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 线程池
    private static final ExecutorService executorService = Executors.newFixedThreadPool(10);

    @Override
    public Result queryById(Long id) {

        // 互斥锁解缓存击穿
        Shop shop = queryWithMutex(id);

        // 用逻辑时间解决缓存击穿
//        Shop shop = queryWithExpireTime(id);
        if (shop == null){
            return Result.fail("商铺不存在");
        }

        return Result.ok(shop);
    }

    /**
     * 互斥锁解决缓存击穿
     * @param id
     * @return
     */
    private Shop queryWithMutex(Long id){
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopStr)){
            return JSONUtil.toBean(shopStr, Shop.class);
        }

        // 判断缓存是否是空值，第一次进来shopStr为null，其余之后就为空串
        if (shopStr != null){
            return null;
        }

        Shop shop;
        try {
            // 上互斥锁
            Boolean lock = lock(id);
            if (!lock){
                // 随眠10ms后递归调用，直到获取锁
                Thread.sleep(10);
                return queryWithMutex(id);
            }
            shop = this.baseMapper.selectById(id);
            // 模拟重建的延迟
//            Thread.sleep(200);
            if (shop == null){
                // 缓存空对象，防止缓存穿透
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 保存缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (Exception e){
            throw new RuntimeException(e);
        } finally {
            // 释放互斥锁
            unlock(id);
        }

        return shop;
    }

    /**
     * 解决缓存穿透
     * @param id
     * @return
     */
    private Shop queryShop(Long id){
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopStr)){
            return JSONUtil.toBean(shopStr, Shop.class);
        }
        // 判断缓存是否是空值，第一次进来shopStr为null，其余之后就为空串
        if (shopStr != null){
            return null;
        }
        Shop shop = this.baseMapper.selectById(id);
        if (shop == null){
            // 缓存空对象，防止缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return shop;
    }

    /**
     * 用逻辑过期时间解决缓存击穿
     * @param id
     * @return
     */
    private Shop queryWithExpireTime(Long id){
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);
        // 若为空直接范围null，因为热点key已经提前预热了的，不可能没有
        if (StrUtil.isBlank(shopStr)){
            return null;
        }
        RedisData redisData = JSONUtil.toBean(shopStr, RedisData.class);
        Shop getShop = BeanUtil.copyProperties(redisData.getData(), Shop.class);
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
            // 未过期，返回之间的数据
            return getShop;
        }
        // 获取锁
        if (lock(id)){
            // 已过期
            // 使用线程来提交任务，从而减少响应时间
            executorService.submit(() -> {
               try {
                   // 重建数据
                   this.saveShop(id, 20L);
               } catch (Exception e){
                   e.printStackTrace();
               } finally {
                   // 释放锁
                   unlock(id);
               }
            });
        }

        return getShop;
    }


    /**
     * 保存数据【使用逻辑字段解决缓存击穿】
     * @param id
     * @param time
     */
    public void saveShop(Long id, Long time){
        // 查询数据库
        Shop shop = this.baseMapper.selectById(id);
        // 封装逻辑过期字段
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(time));
        // 缓存redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 上锁
     * @param id
     * @return
     */
    private Boolean lock(Long id){
        return stringRedisTemplate.opsForValue().setIfAbsent(LOCK_SHOP_KEY + id, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
    }

    /**
     * 释放锁
     * @param id
     */
    private void unlock(Long id){
        stringRedisTemplate.delete(LOCK_SHOP_KEY + id);
    }

    @Override
    public Result updateShop(Shop shop) {
        Shop getShop = this.baseMapper.selectById(shop.getId());
        if (getShop == null){
            return Result.fail("商铺不存在");
        }
        BeanUtils.copyProperties(shop, getShop);
        this.baseMapper.updateById(shop);
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());

        return Result.ok();
    }
}
